package com.junxonline.instant.admin.shiro;

import cn.hutool.core.date.DateUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.http.HttpStatus;
import com.alibaba.fastjson.JSON;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.junxonline.instant.common.util.common.RedisUtils;
import com.junxonline.instant.dao.entity.model.config.User;
import com.junxonline.instant.dao.mapper.config.UserMapper;
import com.junxonline.instant.common.constant.RedisConstant;
import com.junxonline.instant.common.constant.SQLConstant;
import com.junxonline.instant.common.constant.UserInfoConstant;
import com.junxonline.instant.common.entity.common.CommonResult;
import com.junxonline.instant.common.enumeration.AdminErrorEnum;
import com.junxonline.instant.common.util.admin.TokenUtils;
import com.junxonline.instant.util.CorsUtils;
import lombok.extern.log4j.Log4j2;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.context.annotation.Import;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;

/**
 * 登录认证过滤器
 *
 * @author JunX
 * @date 2021-06-27
 */
@Log4j2
@Import(cn.hutool.extra.spring.SpringUtil.class)
public class AuthFilter extends BasicHttpAuthenticationFilter {

    private TokenUtils tokenUtils;

    private com.junxonline.instant.common.util.common.RedisUtils redisUtils;

    private UserMapper userMapper;

    /**
     * 过滤器前置方法
     *
     * @param request  request
     * @param response response
     * @return boolean
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        // 支持跨域请求
        HttpServletResponse httpServletResponse = CorsUtils.setResponseCorsSupport(request, response);
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.HTTP_OK);
            return false;
        }
        return super.preHandle(httpServletRequest, httpServletResponse);
    }

    /**
     * 判断是否允许通过
     *
     * @param request     request
     * @param response    response
     * @param mappedValue mappedValue
     * @return boolean
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        try {
            // 判断用户是否想要登入
            if (isLoginAttempt(request, response)) {
                this.executeLogin(request, response);
            }
        } catch (Exception e) {
            Throwable cause = e.getCause();
            if (cause instanceof TokenExpiredException) {
                return refreshToken(request, response);
            } else {
                responseUnauthorized(response);
                return false;
            }
        }
        return true;
    }

    /**
     * 登录授权认证
     *
     * @param request  request
     * @param response response
     * @return boolean
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) {
        // 拿到当前request header中Authorization的token
        JwtToken token = new JwtToken(this.getAuthzHeader(request));
        // 提交给Realm进行认证 若发生错误会抛出异常
        this.getSubject(request, response).login(token);
        return true;
    }

    /**
     * 判断请求是否需要校验Token
     *
     * @param request  request
     * @param response response
     * @return boolean
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        String token = this.getAuthzHeader(request);
        return token != null;
    }

    /**
     * 认证失败
     *
     * @param request  request
     * @param response response
     * @return boolean
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) {
        this.sendChallenge(request, response);
        responseUnauthorized(response);
        return false;
    }

    /**
     * 刷新AccessToken，进行判断RefreshToken是否过期，未过期就返回新的Token且继续正常访问
     *
     * @param request  request
     * @param response response
     * @return boolean
     */
    private boolean refreshToken(ServletRequest request, ServletResponse response) {
        if (redisUtils == null) {
            redisUtils = SpringUtil.getBean(RedisUtils.class);
        }
        if (tokenUtils == null) {
            tokenUtils = SpringUtil.getBean(TokenUtils.class);
        }
        if (userMapper == null) {
            userMapper = SpringUtil.getBean(UserMapper.class);
        }
        String token = this.getAuthzHeader(request);
        String username = tokenUtils.getUsername(token);
        Long signTime = tokenUtils.getSignTime(token);
        // 判断Redis中RefreshToken是否存在
        if (redisUtils.hHasKey(RedisConstant.KEY_ONLINE_USER + username, UserInfoConstant.KEY_REFRESH_TOKEN)) {
            // 获取Redis中的RefreshToken的时间戳
            Long refreshTokenTime = (Long) redisUtils.hget(RedisConstant.KEY_ONLINE_USER + username, UserInfoConstant.KEY_REFRESH_TOKEN);
            // 同步锁防止重复刷新
            synchronized (this) {
                // 对比Token和RefreshToken的时间戳 如果时间戳一致则进行Token的刷新
                if (refreshTokenTime.equals(signTime)) {
                    // 获取当前最新时间戳
                    Long currentTime = DateUtil.current();

                    try {
                        // 查询用户密码
                        User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUsername, username).last(SQLConstant.LIMIT_1));
                        if (user.getStatus() == 0) {
                            return false;
                        }
                        // 重新签发Token
                        String newToken = tokenUtils.sign(user.getId(), user.getUsername(), user.getPassword(), currentTime);
                        // 刷新Redis中的RefreshToken时间戳为当前最新时间戳
                        redisUtils.hset(RedisConstant.KEY_ONLINE_USER + username, UserInfoConstant.KEY_REFRESH_TOKEN, currentTime, tokenUtils.getRefreshTokenExpireTime());
                        // 将新生成的Token再次提交给Shiro进行登录认证
                        this.getSubject(request, response).login(new JwtToken(newToken));
                        // 将过期的Token存入Redis中并设置销毁缓冲时间
                        redisUtils.set(RedisConstant.KEY_BUFFER_TOKEN + username, token, tokenUtils.getRefreshTokenExpireBufferTime());
                        // 将新Token放入Response Header中响应
                        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
                        httpServletResponse.setStatus(HttpStatus.HTTP_OK);
                        httpServletResponse.setHeader("Authorization", newToken);
                        httpServletResponse.setHeader("Access-Control-Expose-Headers", "Authorization");
                        return true;
                    } catch (Exception e) {
                        e.printStackTrace();
                        return false;
                    }

                } else {
                    // 当RefreshToken信息不一致时说明Token已经被刷新
                    // 在并发请求时过期的token可在缓冲时间内重新通过认证
                    return expiredTokenBuffHandler(username, token);
                }
            }
        }
        return false;
    }

    /**
     * 在缓冲时间内过期token可重新通过认证
     *
     * @param username     用户名
     * @param expiredToken 过期Token
     */
    private boolean expiredTokenBuffHandler(String username, String expiredToken) {
        // 判断Redis中是否存在缓冲Token
        boolean isExist = redisUtils.hasKey(RedisConstant.KEY_BUFFER_TOKEN + username);
        // 判断Token是否一致
        if (isExist) {
            String bufferToken = (String) redisUtils.get(RedisConstant.KEY_BUFFER_TOKEN + username);
            return bufferToken.equals(expiredToken);
        }
        return false;
    }

    /**
     * 认证失败响应
     *
     * @param response response
     */
    private void responseUnauthorized(ServletResponse response) {
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setStatus(HttpStatus.HTTP_OK);
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json;charset=UTF-8");
        OutputStream out = null;
        try {
            out = httpServletResponse.getOutputStream();
            CommonResult result = CommonResult.fail(AdminErrorEnum.AUTH_FAIL.getCode(), AdminErrorEnum.AUTH_FAIL.getMsg());
            String resultJson = JSON.toJSONString(result);
            out.write(resultJson.getBytes());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (out != null) {
                    out.close();
                    out = null;
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}
